package com.lzx.hbh_system.shiro;

import com.auth0.jwt.exceptions.TokenExpiredException;
import com.lzx.hbh_system.bo.SysPermission;
import com.lzx.hbh_system.bo.SysRole;
import com.lzx.hbh_system.bo.SysUser;
import com.lzx.hbh_system.bo.filter.Userfilter;
import com.lzx.hbh_system.dto.UserDto;
import com.lzx.hbh_system.service.PermissionService;
import com.lzx.hbh_system.service.RoleService;
import com.lzx.hbh_system.service.UserService;
import com.lzx.hbh_system.util.JWTUtil;
import com.lzx.hbh_system.util.Md5Utils;
import com.lzx.hbh_system.util.RedisUtils;
import com.lzx.hbh_system.util.SerializeUtil;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.ObjectUtils;

import java.util.Date;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

@Slf4j
public class JWTRealm extends AuthorizingRealm {
    @Autowired
    private RedisUtils redisUtils;
    @Autowired
    private RoleService roleService;
    @Autowired
    private PermissionService permissionService;

    @Override
    public void setName(String name) {
        super.setName("REALM");
    }

    public JWTRealm(){
        //重写判断token方法，刚刚实现的JWTCredentialsMatcher类进行判断
        //实例化时，设置刚刚我们重写的token验证类，用于验证jwttoken
        //密码比对器
        this.setCredentialsMatcher(new JWTCredentialsMatcher());
    }
    /**
     * 因为是自实现验证token的累替代了默认的UsernamePasswordToken 所以要我们手动判断类型，
     * 否则因为默认判断传入的token判断类是否与默认的UsernamePasswordToken类型不匹配导致报错，
     *
     * 判断是否为JWTToken类型
     * 是 则 允许访问
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JWTToken;
    }

    /**
     * 执行授权逻辑-权限验证，当使用了@RequiresAuthentication、@RequiresRoles等shiro注解才会进行调用
     * 权限判断时授权才会调用该方法，查询对应用户所拥有的权限和角色 列表
     * @param principalCollection
     * @return
     */
    @SneakyThrows
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        log.info("执行授权逻辑");
        String username = JWTUtil.getUsername(principalCollection.toString());
        //获取该用户对应的角色
        SysRole role = roleService.getRoleByUsername(username);
        //获取该角色对应的权限列表
        List<SysPermission> permissionList = permissionService.getPermissionByUsername(username);
        SimpleAuthorizationInfo s = new SimpleAuthorizationInfo();
        if(!(ObjectUtils.isEmpty(role))){
            s.addRole(role.getRoleName());
        }else {
            return null;
        }
        Set<String> permissionSet = new LinkedHashSet<>();//保证有序
        if (!permissionList.isEmpty() && permissionList.get(0) != null ){
            permissionList.stream().forEach(permissionObject->{
                //放置对应权限名称，有序不重复。
                permissionSet.add(permissionObject.getPermissionName().trim());
            });
            s.setStringPermissions(permissionSet);
        }
        return s;
    }

    /**
     * 执行认证逻辑-登陆验证-会调用JWTCredentialsMatcher进行匹配-验证token有效性
     *
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    /*
    DisabledAccountException （禁用的帐号）
    LockedAccountException （锁定的帐号）
    UnknownAccountException（错误的帐号）
    ExcessiveAttemptsException（登录失败次数过多）
    IncorrectCredentialsException （错误的凭证）
    ExpiredCredentialsException （过期的凭证）
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException{
        log.info("执行认证逻辑");
        //从重写的JWTToken类中获取"executelogin的getSubject(request,response).login(jwtToken)"携带的的newtoken
        String token = (String) authenticationToken.getCredentials();
        String username = JWTUtil.getUsername(token);
        if(username == null){//凭证异常
            throw new CredentialsException("CredentialsException");
        }
        //获取数据库中的用户
        /*
        * 问题：使用了JWT验证token，还是查询了数据库，违背了jwt无状态思路
        * 可以把用户的salt值存放在redis中，登陆的时候存放进去，验证时，获取redis中的用户对应的salt值使用MD5对 （salt+用户名） 或者 (salt+用户密码)加密匹配
        * 步骤：1 验证时，从token中拿到username
        *      2 用username从redis中获取对应salt值，redis中可以设置key有效期，惰性删除(get时检查key有效性)过期对应内容，若不存在，则说明用户失效，登陆失败，存在则继续进行
        *      3 使用JWTuitl匹配salt，和 token
        *      4 true则token有效，访问成功
        * 优点：减少访问数据库的次数，提升查询效率，加强安全性
        * 缺点：还是用到了redis，一定意义上违背无状态JWT。
        * */
        Boolean hasUserSalt = redisUtils.hasKey(username);
        if(!hasUserSalt){//未知账户
            throw new AuthenticationException("UserLoginStatusTimeOutException");
        }
        UserDto userDto = (UserDto) redisUtils.getObject(username);
        //获取redis的登陆user的salt值
        String salt = userDto.getSalt();
        //生成secret
        String secret = Md5Utils.MD5(salt+username);
        System.out.println("从redis中信息生成的secret："+secret);
        if (userDto.getStatus().equals("0")){//0 账号冻结 1 账号正常 01 用户欠费  00 用户注销-vali设为 0
            throw new LockedAccountException("LockedAccountException");
        }
        if(!JWTUtil.verify(token,username,secret)){//凭证异常
            throw new IncorrectCredentialsException("TokenExpiredException");
        }
        return new SimpleAuthenticationInfo(token,token,getName());
    }
}
